import numpy as np
import matplotlib.pyplot as plt


def get_samples_key(average, std_div, num):    # 随机生成符合条件的样本数量
    # 传入样本均值，标准差，样本数量，随机生成样本属性集合
    x_class = np.random.normal(loc=average, scale=std_div, size=(num, 2))
    return x_class


def generate_samples(num_samples, noise=0.1):  # 生成样本，数量为num_samples，噪声范围为 0 ~ noise
    # 生成类别1的样本
    np.random.seed(0)
    num_samples_class1 = int(num_samples / 2)
    x_class1 = get_samples_key(2, 1, num_samples_class1)
    # 类别1的样本标签设置为0
    y_class1 = np.zeros((num_samples_class1,))

    # 生成类别2的样本
    num_samples_class2 = num_samples - num_samples_class1
    x_class2 = get_samples_key(-2, 1, num_samples_class2)
    # 类别2的样本标签设置成1
    y_class2 = np.ones((num_samples - num_samples_class1,))

    # 添加噪声
    x_class1 += np.random.normal(scale=noise, size=x_class1.shape)
    x_class2 += np.random.normal(scale=noise, size=x_class2.shape)

    # 合并样本
    x = np.vstack((x_class1, x_class2))       # 将x属性值堆叠起来
    y = np.concatenate((y_class1, y_class2))  # 将两个一维数组的标签拼接起来

    return x, y


# 绘制模型（样本以及决策边界）


def write_model(x, y, xx1, xx2, predictions):
    plt.figure(figsize=(8, 6))
    # 绘制边界，并且用颜色填充，使用coolwarm颜色渐变进行填充，透明度为0.3
    plt.contourf(xx1, xx2, predictions, alpha=0.3, cmap='coolwarm')
    # 取出所有类别1的样本，设置标签class 0，颜色为蓝色
    plt.scatter(x[y == 0][:, 0], x[y == 0][:, 1],
                color='blue', label='Class 0')
    # 取出所有类别2的样本，设置标签class 1，颜色为红色
    plt.scatter(x[y == 1][:, 0], x[y == 1][:, 1],
                color='red', label='Class 1')
    plt.title('Logistic Regression')
    plt.xlabel('X1')
    plt.ylabel('X2')
    plt.legend()     # 设置图例
    plt.grid(True)
    plt.show()

# 定义对数几率函数


def sigmoid(z):
    return 1 / (1 + np.exp(-z))


"""""
对数似然函数梯度
"""""


def log_likelihood_gradient(x, y, w, b):
    z = np.dot(x, w) + b
    y_pred = sigmoid(z)
    # 通过将输入特征 x 转置后与预测值和实际值之间的差异相乘再求和得到关务w的梯度
    dw = np.dot(x.T, (y_pred - y))
    # 计算预测值与真实值之间的差异得到关于b的梯度
    db = np.sum(y_pred - y)
    return dw, db

# 对数几率回归训练函数


def logistic_regression_train(x, y, learning_rate=0.01, num_iterations=1000):
    # 初始化参数
    num_samples, num_features = x.shape
    w = np.zeros(num_features)
    b = 0
    # 梯度下降求解极大似然函数的最大值
    for i in range(num_iterations):
        # 计算对数似然函数的梯度
        dw, db = log_likelihood_gradient(x, y, w, b)
        # 更新参数
        w -= learning_rate * dw
        b -= learning_rate * db
    return w, b


# 对数几率回归预测函数
def logistic_regression_predict(x, w, b):
    z = np.dot(x, w) + b
    y_pred = sigmoid(z)
    # 对预测值进行四舍五入，从而得到该点对应的类别预测
    y_pred_class = np.round(y_pred)
    return y_pred_class


if __name__ == "__main__":
    # 生成样本
    num_samples = 100
    x, y = generate_samples(num_samples, 0.5)

    # 训练对数几率回归模型
    w, b = logistic_regression_train(x, y)

    # 绘制决策边界
    x1_min, x1_max = x[:, 0].min() - 1, x[:, 0].max() + 1
    x2_min, x2_max = x[:, 1].min() - 1, x[:, 1].max() + 1
    # 得到所有点的排列组合
    xx1, xx2 = np.meshgrid(np.arange(x1_min, x1_max, 0.1),
                           np.arange(x2_min, x2_max, 0.1))
    # ravel方法将二维数组拉平成一维数组，并且通过调用numpy的c_[]方法将两个一维数组以[x1, x2]的方式按列连接起来
    grid_points = np.c_[xx1.ravel(), xx2.ravel()]
    # 对所有点使用对数几率函数测得标签值
    predictions = logistic_regression_predict(grid_points, w, b)
    # 将标签数组改变成二维的形状，便于绘制决策边界
    predictions = predictions.reshape(xx1.shape)

    # 绘制样本和决策边界
    write_model(x, y, xx1, xx2, predictions)
